package com.example.basic.snowflake;

import java.time.Duration;
import java.time.Instant;

/**
 * Represents a unique id generator.
 *
 * @author yutianbao
 */
public class DefaultIdGenerator implements IdGenerator {
    private final OffsetClock offsetClock;
    private final int workerId;
    private final BitsAllocator bitsAllocator;

    private long sequence = 0L;
    private long lastOffset = -1L;

    /**
     * @param offsetClock 时间偏移量
     * @param workerId workerId
     * @param bitsAllocator 位数分配器
     */
    public DefaultIdGenerator(OffsetClock offsetClock, int workerId, BitsAllocator bitsAllocator) {
        this.offsetClock = offsetClock;
        this.workerId = workerId;
        this.bitsAllocator = bitsAllocator;
    }

    public long getUID() {
        return nextId();
    }

    protected synchronized long nextId() {
        long currentOffset = getCurrentOffset();
        if (currentOffset < lastOffset) {
            // 时钟回拨
            long refusedOffset = lastOffset - currentOffset;
            throw new IdGenerateException("Clock moved backwards. Refusing for %d offset", refusedOffset);
        }
        if (currentOffset == lastOffset) {
            // 相同偏移量, 序列自增
            long seq = (sequence + 1) & bitsAllocator.getMaxSequence();
            if (seq == 0) {
                // 已经达到最大序列
                throw new IdGenerateException("workerId [%s] sequence over", workerId);
            }
            sequence = seq;
        } else {
            sequence = 0L;
        }
        lastOffset = currentOffset;
        // 分配ID
        return bitsAllocator.allocate(currentOffset, workerId, sequence);
    }

    /**
     * Get current offset
     */
    private long getCurrentOffset() {
        long currentOffset = offsetClock.get();
        if (currentOffset > bitsAllocator.getMaxTimeOffset()) {
            throw new IdGenerateException("Timestamp bits is exhausted. Max: %s Now: %s", bitsAllocator.getMaxTimeOffset(), currentOffset);
        }
        return currentOffset;
    }

    @Override
    public int getWorkerId() {
        return workerId;
    }

    @Override
    public String parseUID(long uid) {
        long totalBits = bitsAllocator.getTotalBits();
        long signBits = bitsAllocator.getSignBits();
        long timestampBits = bitsAllocator.getTimestampBits();
        long workerIdBits = bitsAllocator.getWorkerIdBits();
        long sequenceBits = bitsAllocator.getSequenceBits();
        // parse UID
        long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
        long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
        long deltaOffset = uid >>> (workerIdBits + sequenceBits);
        Instant date = offsetClock.parse(deltaOffset).plus(Duration.ofHours(8));
        // format as string
        return String.format("{UID:%d,timestamp(%s),workerId(%d),sequence(%d)}", uid, date.toString(), workerId, sequence);
    }
}
